home *** CD-ROM | disk | FTP | other *** search
Wrap
/* TableDataSource.m * * This data source reads a flat file table (such as Product.table or Item.table) * and generates EOGeneric records that can be passed to an EOController. * * You may freely copy, distribute, and reuse the code in this example. * NeXT disclaims any warranty of any kind, expressed or implied, as to its * fitness for any particular use. * * * */ #import <appkit/appkit.h> #import <eoaccess/eoaccess.h> #import "TableDataSource.h" #import "TableDataSourcePrivate.h" #import "DetailTableDataSource.h" #include <sys/file.h> #define @QUALIFIER_ALL @"*" #define @QPROPERTY @"PROPERTY" #define @QVALUE @"VALUE" @implementation TableDataSource // init a new TableDataSource with an array of EOGeneric records and the path to // the table file (the flat file database!). The entity name will be found from a // record in the array. // Use this initialization method when you have existing records (possibly from // a real database) and you want to save them to a file - initWithEOGenericRecords:(NSArray *)records tablePath:(NSString *)tablePath { NSString *tableFile; NSMutableArray *dictArray = [NSMutableArray array]; NSDictionary *dict; int i; // We need at least a record to get the entity if (![records count]) { NSLog(@"<initWithEOGenericRecords:tablePath:> No records"); return nil; } entity = [[records objectAtIndex:0] entity]; tableFile=[NSString stringWithFormat:@"%@/%@.table", tablePath, [entity name]]; for (i=0; i<[records count]; i++) { dict = [[records objectAtIndex:i] valuesForKeys:[entity classPropertyNames]]; [dictArray addObject:dict]; } // Save the records to a file as a property-list string if (![[[dictArray description] dataUsingEncoding:NSASCIIStringEncoding] writeToFile:tableFile atomically:NO]) { NSLog(@"<initWithEOGenericRecords:tablePath:> DataSource %@ failed to save", [entity name]); return nil; } // Call the designated initialization method else return [self initWithEntity:entity tablePath:tablePath]; } // The designated initialization method for TableDataSource // Pick an entity in the model, then initialize the source from a flat file that // contains a string representation of an array of dictionaries. // Note that unlike the EODatabaseSataSource, this dataSource has a "state". // It cashes the eos (dictionaries) that it read from the file and also maintains // a few hash tables to speed up fetch operations... // Although the state of the TableDataSource is an array of dictionaries (I call it // the snapshot) the TabledataSource will copy these dictionaries into an array of // EOGeneric records before handing them to a controller... - initWithEntity:(EOEntity *)anEntity tablePath:(NSString *)tablePath { NSString *primaryKey; NSArray *relations; NSMutableArray *qualifiers = [NSMutableArray array]; [super init]; entity = [anEntity retain]; primaryKey = [(EOAttribute *)[[entity primaryKeyAttributes] objectAtIndex:0] name]; relations = [entity relationships]; detailSources = [[NSMutableArray array] retain]; return [self initWithTable:tablePath primaryKey:primaryKey qualifierKeys:qualifiers]; } // The new NSObject world, I guess - (void)dealloc { [table autorelease]; [eos autorelease]; [lookupTables autorelease]; [qualifier autorelease]; [uniqueKey autorelease]; [detailSources autorelease]; [entity autorelease]; [orderByKey autorelease]; [super dealloc]; } // The path to persistent storage, here to the "table" file... - (NSString *)tablePath { return table; } // The other one... - setTablePath:(NSString *)aPath { [table autorelease]; table = [aPath retain]; return self; } // The supported entity, we got it from the EOmodel at initialization time - (EOEntity *)entity { return entity; } // A custom way (meaning different from the EOF way) to qualify the source for a given property name and value // The equivalent SQL is: WHERE( key = value) - qualifyForProperty:(NSString *)key andValue:value { NSMutableDictionary *qual = [NSMutableDictionary dictionary]; if (![[self keys] containsObject:key]) { NSLog(@"%@ is not a valid property of entity %@", key, [entity name]); qual=nil; } else { [qual setObject:key forKey:@QPROPERTY]; [qual setObject:value forKey:@QVALUE]; } return [self setQualifier:qual]; } // Qualify for all records - setEntityQualifier { NSMutableDictionary *qual =[NSMutableDictionary dictionary]; [qual setObject:uniqueKey forKey:@QPROPERTY]; [qual setObject:@QUALIFIER_ALL forKey:@QVALUE]; return [self setQualifier:qual]; } // Set the qualifier before the fetch - setQualifier:(NSMutableDictionary *)newQualifier { [qualifier autorelease]; if (newQualifier) qualifier = [newQualifier retain]; else qualifier=nil; //NSLog(@"New qualifier %@", [(NSMutableDictionary *)qualifier description]); return self; } - setEmptySetQualifier { [qualifier autorelease]; qualifier=nil; return self; } - (BOOL)orderBy:(NSString *)key { if (![[self keys] containsObject:key]) return NO; else { [orderByKey autorelease]; orderByKey = [key retain]; [eos sortUsingFunction:eoSort context:orderByKey]; return YES; } } - setOrderDescendantSources:(BOOL)aFlag { orderDescendantSources=aFlag; return self; } // ************************************* DATASOURCE PROTOCOL ****************/ // The class property names (keys for the dictionary or EOGenericRecord) - (NSArray *)keys { NSArray *attrs = [entity attributes]; NSMutableArray *keys = [NSMutableArray array]; int i; for (i=0; i<[attrs count]; i++) { EOAttribute *at = [attrs objectAtIndex:i]; [keys addObject:[at name]]; } return keys; } - createObject{ NSNumber *newId=[self findNextPrimaryKey]; NSMutableDictionary *newObject=[[NSMutableDictionary alloc] init]; if (!newId) return nil; else { NSArray *allKeys = [self keys]; NSArray *relations = [entity relationships]; int i, count; for (i=0, count=[allKeys count]; i<count; i++) { id key = [allKeys objectAtIndex:i]; [newObject setObject:@"" forKey:key]; } for (i=0, count=[relations count]; i<count; i++) { EORelationship *rel = [relations objectAtIndex:i]; if ([rel isToMany]) [newObject setObject:[newId description] forKey:[rel name]]; } [newObject setObject:[newId description] forKey:uniqueKey]; return newObject; } } - (BOOL)insertObject:object { NSNumber *objectId=[object objectForKey:uniqueKey]; NSArray *allKeys; int i; NSMutableDictionary *newObject; if (![self isValidNewPrimaryKey:objectId]) { NSLog(@"DataSource cannot insert; primary key is invalid: %@", [(NSString *)objectId description]); return NO; } newObject=[NSMutableDictionary dictionary]; allKeys = [self keys]; for (i=0; i<[allKeys count]; i++) { id key = [allKeys objectAtIndex:i]; id value = [object objectForKey:key]; if ([value isEqual:[EONull null]]) value=@""; [newObject setObject:value forKey:key]; } [eos addObject:newObject]; if (orderByKey) [eos sortUsingFunction:eoSort context:orderByKey]; [self modifyLookupTables]; return YES; } - (BOOL)deleteAllObjects { int i; for (i=[eos count]-1; i>=0; i--) { [eos removeObject:[eos objectAtIndex:i]]; } [self modifyLookupTables]; return YES; } - (BOOL)deleteObject:object { id primaryKeyValue = [object objectForKey:uniqueKey]; NSMutableArray *deletes=[[lookupTables objectForKey:uniqueKey] objectForKey:primaryKeyValue]; int i; if ( !deletes || ![deletes count]) return NO; for (i=0; i<[deletes count]; i++) { [eos removeObject:[deletes objectAtIndex:i]]; } [self modifyLookupTables]; return YES; } - (BOOL)updateObject:object { id primaryKeyValue = [object objectForKey:uniqueKey]; NSMutableArray *update=[[lookupTables objectForKey:uniqueKey] objectForKey:primaryKeyValue]; NSArray *allKeys; NSMutableDictionary *eo; int i; NSString *key; id value; if (!update || [update count]!=1) return NO; allKeys = [self keys]; eo = [update objectAtIndex:0]; for (i=0; i<[allKeys count]; i++) { key = [allKeys objectAtIndex:i]; value = [object objectForKey:key]; if (!value || [value isEqual:[EONull null]] ) value=@""; [eo setObject:value forKey:key]; } if (orderByKey) [eos sortUsingFunction:eoSort context:orderByKey]; [self modifyLookupTables]; return YES; } - (NSArray *)fetchObjects { NSArray *records; NSString *qualifierPropertyKey = [qualifier objectForKey:@QPROPERTY]; NSString *qualifierPropertyValue = [qualifier objectForKey:@QVALUE]; if (qualifierPropertyKey && qualifierPropertyValue) { if ( [qualifierPropertyValue isEqual:@QUALIFIER_ALL] ) { records =[self eosArrayToGenericRecordsArray:eos]; } else { NSMutableDictionary *hash = [lookupTables objectForKey:qualifierPropertyKey]; if (!hash) { NSLog(@"No lookup table for qualifier key %@", qualifierPropertyKey); records =nil; } else { NSArray *qualifiedEos=[hash objectForKey:qualifierPropertyValue]; if (!qualifiedEos || ![qualifiedEos count]) { //NSLog(@"No qualified eos where %@=%@", qualifierPropertyKey, [(NSString *)qualifierPropertyValue description]); records =nil; } records =[self eosArrayToGenericRecordsArray:qualifiedEos]; } } } else { //NSLog(@"nil or uncomplete qualifier %@", [(NSMutableDictionary *)qualifier description]); records = [NSArray array]; } //NSLog(@"TABLE %@ - FETCHED OBJECTS: %d", [entity name], [records count]); return records; } // This is where we save the eos to persistent storage (COMMIT in SQL) - (BOOL)saveObjects{ NSString *tableFile=[NSString stringWithFormat:@"%@/%@.table", table, [entity name]]; if (![[[eos description] dataUsingEncoding:NSASCIIStringEncoding] writeToFile:tableFile atomically:NO]) { NSString *emess; if ( (!access([tableFile cString], F_OK)) && (access([tableFile cString], W_OK)) ) { int ans; emess = [NSString stringWithFormat:@"\'%@\' data source cannot save changes. You do not have write permission on \'%@\'. Do you want to overwrite?", [entity name], tableFile]; ans=NXRunAlertPanel("ERROR", [emess cString], "Overwrite", "Cancel", NULL); if (ans==NX_ALERTDEFAULT) return [self forceSaveObjects]; else return NO; } else return NO; } else return YES; } - (BOOL)canDelete { return YES; } // Not implemented yet... // Coerce a value to the appropriate type. // This method should convert to either an NSNumber, NSString, NSData, // a custom type, or nil. The value return by this method may be safely // passed to an EO via takeValuesFromDictionary:. This method is used // by controllers to coerce values supplied from associations before // those values are passed on to the EOs. - coerceValue: value forKey: (NSString *)key { // sorry, another day , maybe... return value; } // ****************************** MASTER DATASOURCE PROTOCOL ****************/ // What we are doing here is closer to a master-peer than a master-detail setup. // We are creating the detail dataSource and handing it to the association. // This new detail dataSource will be attached to the detail controller automatically // From there, the detail controller and its DetailTableDataSource will be on their own // Also notice than in an EOF master-detail setup, a master eo (meaning an eo // produced by the master source) "carries" its detail eos. In other word if one // writes [masterEo objectForKey:masterDetailRelationshipKey], the master eo will // return an array full of detail eos (assuming that the master detail relationship // was set as a class property name in EOModeler). Although it could, the TableDataSource // does not support that feature... - (id <EOQualifiableDataSources>)dataSourceQualifiedByKey:(NSString *)key { EORelationship *rel = [entity relationshipNamed:key]; EOEntity *detailEntity=[rel destinationEntity]; DetailTableDataSource *detailSource; if (!rel) return nil; if (!detailEntity) return nil; detailSource = [[(DetailTableDataSource *)[DetailTableDataSource alloc] initWithMasterDataSource:self entity:detailEntity] autorelease]; if (orderByKey && orderDescendantSources) [detailSource orderBy:orderByKey]; // We cash it (I had some grandiose plane, unused today!!) [detailSources addObject:detailSource]; return detailSource; } // ***************************************************************************/ // a convenience method to fetch all the objects in the entity - (NSArray *)fetchAllObjects { NSMutableDictionary *qual =[NSMutableDictionary dictionary]; [qual setObject:uniqueKey forKey:@QPROPERTY]; [qual setObject:@QUALIFIER_ALL forKey:@QVALUE]; [self setQualifier:qual]; return [self fetchObjects]; } // a convenience fetch method - objectForPrimaryKey:value { id hash = [lookupTables objectForKey:uniqueKey]; NSArray *objects = [hash objectForKey:value]; id object; EOGenericRecord *record; NSMutableDictionary *primaryKeyDictionary; NSString *key; id copy; int count, j; NSArray *allKeys = [self keys]; if ([objects count]!=1) return nil; object = [objects objectAtIndex:0]; primaryKeyDictionary=[NSMutableDictionary dictionary]; [primaryKeyDictionary setObject:value forKey:uniqueKey]; record = [[EOGenericRecord alloc] initWithPrimaryKey:primaryKeyDictionary entity:entity]; for (j=0, count=[allKeys count]; j<count; j++) { key = [allKeys objectAtIndex:j]; copy = [[object objectForKey:key] copy]; [record setObject:[copy autorelease] forKey:key]; } return [record autorelease]; } - addLookupTableForKey:(NSString *)key { NSMutableDictionary *hash; hash = [self createLookupTableForQualifierKey:key]; [lookupTables setObject:hash forKey:key]; return self; } @end // ************************************* Private Category ****************/ @implementation TableDataSource (Private) // This initialization method is called by initWithEntity:tablePath: // It sets the key for the primary key and creates a key->array of objects hash // tables for each element of the qualifiers array. - initWithTable:(NSString *)tablePath primaryKey:(NSString *)primaryKey qualifierKeys:(NSArray *)qualifiers { // Create snapshot from table (eos) table = [tablePath retain]; if (![self createSnapshot]) return nil; // Create a dictionary of hash tables for query (key is like @"ID", value is a lookup table) uniqueKey = [primaryKey retain]; if ( !([self createLookupTables:qualifiers]) ) return nil; // set qualifier to select everything // To be clean, we should have a qualifier Class, oh well...it is a rush job // after all orderByKey=nil; orderDescendantSources = NO; qualifier = [[NSMutableDictionary alloc] init]; [qualifier setObject:uniqueKey forKey:@QPROPERTY]; [qualifier setObject:@QUALIFIER_ALL forKey:@QVALUE]; return self; } // The flat file is read here and the objects that have been retrieved are cashed // in an instance variable (of class NSMutableArray) called eos - createSnapshot { NSString *tableFile=[NSString stringWithFormat:@"%@/%@", table, [entity externalName]]; NSString *tableString; NSArray *nonMutableEos; // read the table file that should be in a property list format // Isn't foundation great or what?!!! if ( !(tableString=[[NSString alloc] initWithContentsOfFile:tableFile]) || !(nonMutableEos = [tableString propertyList]) ) { NSLog(@"<createSnapshot> Cannot init from table %@", tableFile); return nil; } // Make deep mutable copy else { NSEnumerator *eoEnumerator = [nonMutableEos objectEnumerator]; NSDictionary *nonMutableEo; eos = [[NSMutableArray alloc] init]; while ( nonMutableEo = [eoEnumerator nextObject] ) { [eos addObject: [[nonMutableEo mutableCopy] autorelease]]; } } return self; } // Create the lookup tables (to speed up fetch operations) // The qualifiers array contains the property names that we should hash on // a lookup table is in the form key = "a property key" --> value = an array of // dictionaries... - createLookupTables:(NSArray *)qualifiers { int i, count; NSMutableDictionary *hash; NSString *qualifierKey; lookupTables = [[NSMutableDictionary alloc] init]; hash = [self createLookupTableForQualifierKey:uniqueKey]; [lookupTables setObject:hash forKey:uniqueKey]; for (i=0, count=[qualifiers count]; i<count; i++) { qualifierKey = [qualifiers objectAtIndex:i]; if ( ![lookupTables objectForKey:qualifierKey] ) { //NSLog(@"Creating lookup table for qualifierKey %@", qualifierKey); if ( !(hash = [self createLookupTableForQualifierKey:qualifierKey]) ) return nil; [lookupTables setObject:hash forKey:qualifierKey]; } } return self; } // Create a lookup table for a given property name (the key) - createLookupTableForQualifierKey:(NSString *)key { NSMutableDictionary *hash; int i, count; NSDictionary *eo; NSMutableArray *values; id value; hash=[NSMutableDictionary dictionary]; for (i=0, count=[eos count]; i<count; i++) { eo = [eos objectAtIndex:i]; // get the property value for the qualifier key if ( !(value = [eo objectForKey:key]) ) { NSLog(@"<createLookupTableForQualifierKey:> eos dictionary does not respond to qualifier key %@", key); return nil; } // It is the first time that we encounter an eo such that propertyValue(key) = value if ( !(values = [hash objectForKey:value]) ) { values = [NSMutableArray array]; [values addObject:eo]; [hash setObject:values forKey:[(NSObject *)value description]]; } // We have already found an eo such that propertyValue(key) = value else { [values addObject:eo]; } } //NSLog(@"HASH FOR KEY %@\n%@", key, [(NSDictionary *)hash description]); return hash; } // After insert, delete or update opeations, the lookup tables must be refreshed - modifyLookupTables { NSArray *allKeys=[lookupTables allKeys]; int i, count; NSString *key; NSMutableDictionary *hash; for (i=0, count=[allKeys count]; i<count; i++) { key = [allKeys objectAtIndex:i]; [lookupTables removeObjectForKey:key]; hash = [self createLookupTableForQualifierKey:key]; [lookupTables setObject:hash forKey:key]; } return self; } // a method to transform an array of eos (state of the TableDataSource) into // an array of EOGeneric records that we can hand to the outside worls... - (NSMutableArray *)eosArrayToGenericRecordsArray:(NSArray *)eoArray { NSMutableArray *records = [NSMutableArray array]; int i, eoCount; for (i=0, eoCount=[eoArray count]; i<eoCount; i++) { NSMutableDictionary *eo; NSMutableDictionary *primaryKey=[NSMutableDictionary dictionary]; EOGenericRecord *record; NSArray *allKeys; NSArray *relations; int j, count; eo = [eoArray objectAtIndex:i]; allKeys = [self keys]; [primaryKey setObject:[eo objectForKey:uniqueKey] forKey:uniqueKey]; record = [[[EOGenericRecord alloc] initWithPrimaryKey:primaryKey entity:entity] autorelease]; for (j=0, count=[allKeys count]; j<count; j++) { NSString *key = [allKeys objectAtIndex:j]; id copy; copy = [[eo objectForKey:key] copy]; [record setObject:[copy autorelease] forKey:key]; } relations = [entity relationships]; // We support master-details but not flattened attribute for now!!! for (j=0, count=[relations count]; j<count; j++) { EORelationship *rel = [relations objectAtIndex:j]; id copy; if ([rel isToMany]) { copy = [[eo objectForKey:uniqueKey] copy]; [record setObject:[copy autorelease] forKey:[rel name]]; } } [records addObject:record]; } return records; } // Unlike the EODatabaseDataSource, when a TableDataSource creates a new object // It sets the primary key value for the created object. // This method computes the next available primary key (the next in the sequence) - (NSNumber *)findNextPrimaryKey { int i, max=-1; for (i=0; i<[eos count]; i++) { id eo; int current; eo = [eos objectAtIndex:i]; current = [[eo objectForKey:uniqueKey] intValue]; if (current > max) max=current; } return [NSNumber numberWithInt:max+1]; } // make sure the the primary key of a new object handed for insertion has a valid primary key // (in this case, valid means not used by a record stored in the table) - (BOOL)isValidNewPrimaryKey:(NSNumber *)number { int i; for (i=0; i<[eos count]; i++) { id eo; eo = [eos objectAtIndex:i]; if ([number isEqual:[eo objectForKey:uniqueKey]]) return NO; } return YES; } - (BOOL)forceSaveObjects { NSString *tableFile=[NSString stringWithFormat:@"%@/%@.table", table, [entity name]]; NSString *emess=@"Cannot overwrite; changes will not be saved..."; NSString *cmd; cmd = [NSString stringWithFormat:@"mv %@ %@.otto", tableFile, tableFile]; if (system([cmd cString])) { NXRunAlertPanel("ERROR", [emess cString], "OK", NULL, NULL); return NO; } cmd = [NSString stringWithFormat:@"cp %@.otto %@; ", tableFile, tableFile]; if (system([cmd cString])) { NXRunAlertPanel("ERROR", [emess cString], "OK", NULL, NULL); return NO; } cmd = [NSString stringWithFormat:@"rm -f %@.otto; chmod u+w %@", tableFile, tableFile]; system([cmd cString]); if (![[[eos description] dataUsingEncoding:NSASCIIStringEncoding] writeToFile:tableFile atomically:NO]) return NO; else return YES; } int eoSort(id eo1, id eo2, void *context) { NSString *key=(NSString *)context; NSString *val1, *val2; if (!key) return NSOrderedSame; val1 = [eo1 objectForKey:key]; val2 = [eo2 objectForKey:key]; if (!val1 || !val2) return NSOrderedSame; else return [val1 compare:val2]; } @end